package com.spzx.product.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.common.rabbitmq.service.RabbitmqService;
import com.spzx.common.redis.util.RedisLockUtil;
import com.spzx.product.domain.SkuStock;
import com.spzx.product.domain.dto.SkuStockDTO;
import com.spzx.product.mapper.SkuStockMapper;
import com.spzx.product.service.SkuStockService;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class SkuStockServiceImpl extends ServiceImpl<SkuStockMapper , SkuStock>
        implements SkuStockService {
    @Resource
    RedisLockUtil redisLockUtil;
    @Resource
    RabbitmqService rabbitmqService;
    @Resource
    RedisTemplate redisTemplate;
    //验库存锁库存业务
    @Override
    public List<SkuStockDTO> checkAndLockSkuStock(List<SkuStockDTO> skuStockDTOS) {
        //1、遍历验库存对象 根据每个对象查询 sku库存
        //2、判断库存是否足够
        //3、锁定库存：更新可用库存减去 要购买的数量  锁定库存数量加上 要购买的数量
        //验库存锁库存 需要保证原子性 线程安全(不同用户可能购买相同的sku 需要保证数据安全)
        //              还应该保证并发性能较高(不同的用户提交订单购买的商品可能不同 不同商品没有竞争关系)
        //保证线程安全：
        //-1、基于数据库的悲观锁实现：   mysql悲观锁容易出现死锁、锁的范围可能会升级       正式开发中比较少见
        //    一个事务  查询数据时 可以通过mysql的for update加锁 其它事务不能读取数据
        //      例如： select * from sku_stock where sku_id = 1 for update;    -- sku_id为1的这行数据 加上mysql的行级锁 其它事务不能读取使用
        //    事务获取到锁以后  更新数据 可以使用版本号机制(乐观锁):
        //      例如： update  sku_stock  set avaliable_num - skuNum , lock_num + skuNum
        //              where sku_id = skuId and (avalibale_num - skuNum)>=0
        //              上面sql的含义： 当sku的可用库存数量大于要购买的商品数量时 才锁定库存 更新库存
        //    代码中接收到更新sql的结果时判断，如果返回的影响记录的行数>0 表示锁库存成功  否则失败
        //    提交事务/回滚事务  mysql的悲观锁在事务结束后会自动释放！

        //-2、基于redis分布式锁来实现
        //  使用skuId获取分布式锁，保证锁定skuId的库存时 只有一个线程可以执行
        //2.1 遍历每个验库存对象  查询库存  如果库存足够 锁定库存
        skuStockDTOS.forEach(skuStockDTO -> {
            //- 1、加分布式锁
            String lockKey = "spzx:product:stock:lock:"+skuStockDTO.getSkuId();
            String uuid = UUID.fastUUID().toString(true);
            redisLockUtil.lock(lockKey , uuid , 10 );
            try {
                //查询数据库中的库存
                SkuStock skuStock = this.getOne(Wrappers.lambdaQuery(SkuStock.class)
                        .eq(SkuStock::getSkuId, skuStockDTO.getSkuId()));
                //库存足够
                if(skuStock.getAvailableNum() - skuStockDTO.getSkuNum() >=0){
                    //锁定库存
                    skuStock.setAvailableNum(skuStock.getAvailableNum() - skuStockDTO.getSkuNum());
                    skuStock.setLockNum(skuStock.getLockNum()+ skuStockDTO.getSkuNum());
                    this.updateById(skuStock);
                    //设置验库存对象的 状态为成功
                    skuStockDTO.setFlag(true);
                }
            } finally {
                //2、释放分布式锁
                redisLockUtil.unlock(lockKey,uuid);
            }
        });

        //2.2 如果一个订单 有验库存失败的sku：其它sku锁定成功，锁定库存成功的需要释放自己锁定的库存！
        //   提交订单验库存的sku 必须一起成功
        List<SkuStockDTO> checkedSuccessStocks = skuStockDTOS.stream()
                .filter(skuStockDTO -> skuStockDTO.isFlag()).collect(Collectors.toList());
        if(checkedSuccessStocks.size() != skuStockDTOS.size()){ //存在验库存失败的  sku
            //将验库存成功的 锁定的库存释放掉
            checkedSuccessStocks.forEach(skuStockDTO -> {
                //- 1、加分布式锁
                String lockKey = "spzx:product:stock:lock:"+skuStockDTO.getSkuId();
                String uuid = UUID.fastUUID().toString(true);
                redisLockUtil.lock(lockKey , uuid , 10 );
                try {
                    //查询数据库中的库存
                    SkuStock skuStock = this.getOne(Wrappers.lambdaQuery(SkuStock.class)
                            .eq(SkuStock::getSkuId, skuStockDTO.getSkuId()));
                    //释放库存
                    skuStock.setAvailableNum(skuStock.getAvailableNum() + skuStockDTO.getSkuNum());
                    skuStock.setLockNum(skuStock.getLockNum()- skuStockDTO.getSkuNum());
                    this.updateById(skuStock);
                    //设置验库存对象的 状态为成功
                    skuStockDTO.setFlag(true);

                } finally {
                    //2、释放分布式锁
                    redisLockUtil.unlock(lockKey,uuid);
                }
            });
        }else{
            //checkedSuccessStocks 锁定成功的库存，如果使用订单编号 将锁定成功的库存存到redis中就可以关单时获取订单要释放的库存数据
            redisTemplate.opsForValue().set("spzx:order:stock:"+checkedSuccessStocks.get(0).getOrderNo(),
                    checkedSuccessStocks);
            //锁定库存成功： 发送一个延迟关单的消息，30分钟后 如果订单还未支付 需要关闭订单 释放订单锁定的库存
            //1、先创建延迟队列：  业务队列接收延迟关单消息 设置消息过期时间未30分钟
            //                 给业务队列绑定死信队列

            //发送关单消息:  消息内容必须保证可以找到订单、还需要获取之前锁定的库存释放
            rabbitmqService.sendMsg("spzx.close.order.exchange",
                    "spzx.close.order.msg",
                    //消息内容使用订单编号
                    checkedSuccessStocks.get(0).getOrderNo());
            //2、再创建消费者： 监听死信队列，一旦接收到消息，
            // 可以修改订单的状态为关闭状态(spzx-order): 订单关闭成功后 再释放锁定的库存(spzx-product)

        }

        return skuStockDTOS;
    }
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void releaseStock(String orderNo) {
        String key = "spzx:order:stock:" + orderNo;
        List<SkuStockDTO> stocks = (List<SkuStockDTO>) redisTemplate.opsForValue()
                .get(key);


        if(CollectionUtils.isEmpty(stocks)){
            return;
        }
        //释放库存
        stocks.forEach(skuStockDTO -> {
            String lockKey = "spzx:product:stock:lock:"+skuStockDTO.getSkuId();
            String uuid = UUID.fastUUID().toString(true);
            redisLockUtil.lock(lockKey , uuid , 10 );
            try {
                //查询数据库中的库存
                SkuStock skuStock = this.getOne(Wrappers.lambdaQuery(SkuStock.class)
                        .eq(SkuStock::getSkuId, skuStockDTO.getSkuId()));
                //释放库存
                skuStock.setAvailableNum(skuStock.getAvailableNum() + skuStockDTO.getSkuNum());
                skuStock.setLockNum(skuStock.getLockNum()- skuStockDTO.getSkuNum());
                this.updateById(skuStock);

            } finally {
                //2、释放分布式锁
                redisLockUtil.unlock(lockKey,uuid);
            }
        });

        //库存全部释放成功：删除redis中锁定库存的缓存
        redisTemplate.delete(key);
    }
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateStockAndSales(String orderNo) {
        String key = "spzx:order:stock:" + orderNo;
        //1、获取锁定的库存
        List<SkuStockDTO> stocks = (List<SkuStockDTO>) redisTemplate.opsForValue()
                .get(key);
        //2、更新库存和销量
        stocks.forEach(skuStockDTO -> {
            String lockKey = "spzx:product:stock:lock:"+skuStockDTO.getSkuId();
            String uuid = UUID.fastUUID().toString(true);
            redisLockUtil.lock(lockKey , uuid , 10 );
            try {
                // 锁定库存时：  lock_num+skuNum , avaliable_num-skuNum
                // 释放库存：  lock_num-skuNum, avaliable_num+skuNum
                // 扣减库存和增加销量：支付成功  lock_num-skuNum , sale_num + skuNum
                //查询数据库中的库存
                SkuStock skuStock = this.getOne(Wrappers.lambdaQuery(SkuStock.class)
                        .eq(SkuStock::getSkuId, skuStockDTO.getSkuId()));
                skuStock.setSaleNum(skuStock.getSaleNum() + skuStockDTO.getSkuNum());
                skuStock.setLockNum(skuStock.getLockNum()- skuStockDTO.getSkuNum());
                this.updateById(skuStock);

            } finally {
                //2、释放分布式锁
                redisLockUtil.unlock(lockKey,uuid);
            }
        });

        //库存全部使用：删除缓存
        redisTemplate.delete(key);
    }
}
